feat(btcindexer): finalize redeems on Sui using BTC confirmations#333
feat(btcindexer): finalize redeems on Sui using BTC confirmations#333
Conversation
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Reviewer's GuideRefactors BTC indexer confirmation/finalization logic to support both mint and redeem flows with shared reorg/confirmation handling, and wires Sui indexer RPC/storage to fetch confirming redeems, update their status, and finalize them on Sui using generated Merkle proofs. Sequence diagram for redeem finalization with shared BTC confirmation logicsequenceDiagram
actor Cron
participant BtcIndexer as Indexer
participant BtcDB as CFStorage
participant SuiRPC as SuiIndexerRpc
participant SuiDB as D1Storage
participant SuiClient as SuiClientImp
participant SuiChain as Sui_network
Cron->>BtcIndexer: updateConfirmationsAndFinalize()
BtcIndexer->>BtcIndexer: verifyConfirmingBlocks()
Note over BtcIndexer: processRedeemFinalization
BtcIndexer->>BtcIndexer: collect btcNetworks from packageConfigs
loop per btcNetwork
BtcIndexer->>BtcDB: getChainTip(network)
alt chainTip is null
BtcIndexer-->>BtcIndexer: skip network
else chainTip exists
BtcIndexer->>SuiRPC: getConfirmingRedeems(network)
SuiRPC->>SuiDB: getConfirmingRedeems(network)
SuiDB-->>SuiRPC: ConfirmingRedeemReq[]
SuiRPC-->>BtcIndexer: ConfirmingRedeemReq[]
BtcIndexer->>BtcIndexer: build ConfirmingTxCandidate[]
BtcIndexer->>BtcIndexer: categorizeConfirmingTxs(candidates, chainTips)
BtcIndexer-->>BtcIndexer: reorged[], finalized[]
loop each reorged
BtcIndexer->>SuiRPC: updateRedeemStatus(redeemId, Reorg)
SuiRPC->>SuiDB: updateRedeemStatus(redeemId, Reorg)
end
BtcIndexer->>BtcIndexer: groupTransactionsByBlock(finalized)
BtcIndexer-->>BtcIndexer: Map blockHash -> redeems
loop per blockHash
BtcIndexer->>BtcDB: fetchAndVerifyBlock(blockHash)
BtcDB-->>BtcIndexer: block, merkleTree
loop per redeem in block
BtcIndexer->>BtcIndexer: find txIndex in block
BtcIndexer->>BtcIndexer: getTxProof(merkleTree, tx)
BtcIndexer-->>BtcIndexer: proofHex[]
BtcIndexer->>BtcIndexer: add FinalizeRedeemItem to batch
end
alt batch not empty
BtcIndexer->>SuiRPC: finalizeRedeems(batch)
SuiRPC->>SuiDB: getRedeemWithSetup(redeemId) for each
SuiDB-->>SuiRPC: RedeemRequest
SuiRPC->>SuiClient: finalizeRedeem(FinalizeRedeemCall)
SuiClient->>SuiChain: signAndExecuteTransaction(finalizeRedeem)
SuiChain-->>SuiClient: digest (success)
SuiClient-->>SuiRPC: digest
SuiRPC->>SuiDB: setRedeemFinalized(redeemId)
end
end
end
end
BtcIndexer-->>Cron: done
Class diagram for updated BTC and Sui indexer confirmation/finalization flowclassDiagram
class Indexer {
- CFStorage storage
- SuiIndexerRpc suiIndexer
- Map~string, DepositInfo~ nbtcDepositAddrMap
- Map~number, PackageConfig~ #packageConfigs
- number confirmationDepth
+ updateConfirmationsAndFinalize() Promise~void~
+ processMintingFinalization() Promise~void~
+ processRedeemFinalization() Promise~void~
- isReorged(height number, oldHash string, network BtcNet) Promise~boolean~
- categorizeConfirmingTxs(txs ConfirmingTxCandidate~T~[], chainTips Map~BtcNet, number~) Promise~ReorgFinalizeResult~
+ splitActiveInactiveTxs(pendingTxs PendingTx[]) ActiveInactiveResult
- groupTransactionsByBlock(items FinalizedRedeemLike[]) Map~string, FinalizedRedeemLike[]~
- fetchAndVerifyBlock(blockHash string) Promise~VerifiedBlockData or null~
- getTxProof(merkleTree MerkleTree, tx Transaction) string[] or null
}
class ConfirmingTxCandidate~T~ {
+ string or number id
+ number blockHeight
+ string blockHash
+ BtcNet network
+ T original
}
class ActiveInactiveResult {
+ string[] activeTxIds
+ string[] inactiveTxIds
}
class ReorgFinalizeResult~T~ {
+ ConfirmingTxCandidate~T~[] reorged
+ ConfirmingTxCandidate~T~[] finalized
}
class PendingTx {
+ string tx_id
+ number block_height
+ string or null block_hash
+ BtcNet btc_network
+ string deposit_address
}
class DepositInfo {
+ number setup_id
+ boolean is_active
}
class PackageConfig {
+ boolean is_active
+ BtcNet btc_network
}
class SuiIndexerRpc {
<<interface>>
+ finalizeRedeems(requests FinalizeRedeemItem[]) Promise~void~
+ putRedeemTx(setupId number, suiTxId string, e RedeemRequestEventRaw) Promise~void~
+ getBroadcastedRedeemTxIds(network string) Promise~string[]~
+ confirmRedeem(txIds string[], blockHeight number, blockHash string) Promise~void~
+ redeemsBySuiAddr(setupId number, suiAddr string) Promise~RedeemRequestResp[]~
+ getConfirmingRedeems(network string) Promise~ConfirmingRedeemReq[]~
+ updateRedeemStatus(redeemId number, status RedeemRequestStatus) Promise~void~
}
class RPC {
<<implements SuiIndexerRpc>>
- Env env
+ finalizeRedeems(requests FinalizeRedeemItem[]) Promise~void~
+ updateRedeemStatus(redeemId number, status RedeemRequestStatus) Promise~void~
+ getConfirmingRedeems(network string) Promise~ConfirmingRedeemReq[]~
+ putRedeemTx(setupId number, suiTxId string, e RedeemRequestEventRaw) Promise~void~
+ getBroadcastedRedeemTxIds(network string) Promise~string[]~
+ confirmRedeem(txIds string[], blockHeight number, blockHash string) Promise~void~
+ redeemsBySuiAddr(setupId number, suiAddr string) Promise~RedeemRequestResp[]~
}
class D1Storage {
- D1Database db
+ getConfirmingRedeems(network string) Promise~ConfirmingRedeemReq[]~
+ updateRedeemStatus(redeemId number, status RedeemRequestStatus) Promise~void~
+ setRedeemFinalized(redeemId number) Promise~void~
+ getRedeemWithSetup(redeemId number) Promise~RedeemRequest or null~
+ getPendingRedeems() Promise~RedeemRequest[]~
+ getBroadcastedRedeems() Promise~RedeemRequest[]~
}
class RedeemRequest {
+ number redeem_id
+ number setup_id
+ string redeemer
+ Uint8Array recipient_script
+ string amount
+ RedeemRequestStatus status
+ number created_at
+ string nbtc_pkg
+ string nbtc_contract
+ string lc_pkg
+ string lc_contract
+ SuiNet sui_network
}
class RedeemRequestRow {
+ number redeem_id
+ number setup_id
+ string redeemer
+ ArrayBuffer or Uint8Array recipient_script
+ string amount
+ number created_at
+ string nbtc_pkg
+ string nbtc_contract
+ string lc_pkg
+ string lc_contract
+ string sui_network
}
class ConfirmingRedeemReq {
+ number redeem_id
+ string btc_tx
+ number btc_block_height
+ string btc_block_hash
+ string btc_network
}
class FinalizeRedeemItem {
+ number redeemId
+ string[] proof
+ number height
+ number txIndex
}
class FinalizeRedeemCall {
+ number redeemId
+ string[] proof
+ number height
+ number txIndex
+ string nbtcPkg
+ string nbtcContract
+ string lcContract
+ string lcPkg
}
class SuiClient {
<<interface>>
+ proposeRedeemUtxos(args ProposeRedeemCall) Promise~string~
+ solveRedeemRequest(args SolveRedeemCall) Promise~string~
+ finalizeRedeem(args FinalizeRedeemCall) Promise~string~
+ requestIkaPresign() Promise~string~
+ requestInputSignature(redeemId number, inputIndex number) Promise~string~
}
class SuiClientImp {
- SuiClientInner #sui
- Keypair signer
+ finalizeRedeem(args FinalizeRedeemCall) Promise~string~
+ proposeRedeemUtxos(args ProposeRedeemCall) Promise~string~
+ solveRedeemRequest(args SolveRedeemCall) Promise~string~
+ requestIkaPresign() Promise~string~
+ requestInputSignature(redeemId number, inputIndex number) Promise~string~
}
Indexer --> SuiIndexerRpc : uses
Indexer --> ConfirmingTxCandidate : creates
Indexer --> PendingTx : finalizes
Indexer --> DepositInfo : checks activity
Indexer --> PackageConfig : reads
RPC --> D1Storage : uses
RPC --> SuiClientImp : uses via createSuiClients
RPC ..|> SuiIndexerRpc
D1Storage --> RedeemRequestRow : maps from
D1Storage --> RedeemRequest : returns
D1Storage --> ConfirmingRedeemReq : returns
SuiClientImp ..|> SuiClient
SuiClientImp --> FinalizeRedeemCall : uses
RPC --> FinalizeRedeemItem : consumes
RPC --> ConfirmingRedeemReq : consumes
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
@sourcery-ai title |
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
categorizeConfirmingTxseach transaction callsisReorged, which in turn hitsgetBlockHashone-by-one; consider grouping by(blockHeight, network)and fetching hashes once per group to avoid an N-per-block D1 query pattern as the pending set grows. - In
RPC.finalizeRedeemsyou fetch redeem details and hit storage perredeemId, then create Sui clients after collecting networks; you could reduce DB and client setup overhead by fetching allredeemIds in a single query (e.g.WHERE redeem_id IN (...)) and constructing the client map once up front.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `categorizeConfirmingTxs` each transaction calls `isReorged`, which in turn hits `getBlockHash` one-by-one; consider grouping by `(blockHeight, network)` and fetching hashes once per group to avoid an N-per-block D1 query pattern as the pending set grows.
- In `RPC.finalizeRedeems` you fetch redeem details and hit storage per `redeemId`, then create Sui clients after collecting networks; you could reduce DB and client setup overhead by fetching all `redeemId`s in a single query (e.g. `WHERE redeem_id IN (...)`) and constructing the client map once up front.
## Individual Comments
### Comment 1
<location> `packages/sui-indexer/src/rpc.ts:39-41` </location>
<code_context>
+ const detailsMap = new Map<number, RedeemRequest>();
+ const networks = new Set<SuiNet>();
+
+ for (const req of requests) {
+ try {
+ const details = await storage.getRedeemWithSetup(req.redeemId);
+ if (details) {
+ detailsMap.set(req.redeemId, details);
</code_context>
<issue_to_address>
**suggestion (performance):** Per-request `getRedeemWithSetup` in a loop will not scale well for large batches.
`finalizeRedeems` issues one `storage.getRedeemWithSetup` call per request inside the loop, causing a linear number of D1 round-trips for large batches. Since all `redeemId`s are known upfront, consider adding a batched query (e.g. `WHERE redeem_id IN (...)`) or at least fetching in parallel (e.g. `Promise.all` with bounded concurrency) to reduce latency and load for large batches.
</issue_to_address>
### Comment 2
<location> `packages/sui-indexer/src/models.ts:167-175` </location>
<code_context>
nbtcContract: string;
}
+
+export interface FinalizeRedeemCall {
+ redeemId: number;
+ proof: string[]; // hex encoded
+ height: number;
+ txIndex: number;
+ nbtcPkg: string;
+ nbtcContract: string;
+ lcContract: string;
+ lcPkg: string;
+}
</code_context>
<issue_to_address>
**suggestion:** `FinalizeRedeemCall.lcPkg` is unused in the Sui client implementation.
`lcPkg` is defined on `FinalizeRedeemCall` but never used in `SuiClientImp.finalizeRedeem`, which only references `nbtcPkg` and `lcContract`. Since `rpc.finalizeRedeems` still populates `lcPkg`, this adds unused data to the call chain. Either remove `lcPkg` from the interface if it’s not needed, or wire it into the transaction builder if it’s intended to be used (e.g., for a separate light client package).
Suggested implementation:
```typescript
export interface FinalizeRedeemCall {
redeemId: number;
proof: string[]; // hex encoded
height: number;
txIndex: number;
nbtcPkg: string;
nbtcContract: string;
lcContract: string;
}
```
1. Update any code that constructs `FinalizeRedeemCall` (e.g., in `rpc.finalizeRedeems` and any other callers) to stop providing `lcPkg`.
2. If there are types or schemas elsewhere (e.g., API DTOs, JSON schemas) mirroring `FinalizeRedeemCall`, remove `lcPkg` from those as well to keep everything consistent.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Signed-off-by: sczembor <43810037+sczembor@users.noreply.github.com>
Signed-off-by: sczembor <43810037+sczembor@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This pull request implements BTC confirmation-based finalization for redeem requests on Sui, extending the existing mint finalization flow with unified reorg detection and confirmation handling logic.
Changes:
- Implements redeem finalization flow that uses BTC Merkle proofs to finalize redeems on Sui once BTC transactions achieve sufficient confirmations
- Refactors confirmation and reorg detection logic into reusable generic helpers that work for both mints and redeems
- Adds per-network chain tip tracking to replace global latest height parameter in the BTC indexer cron flow
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/sui-indexer/src/models.ts | Added new types for confirming redeems, finalize redeem items, and light client fields; removed dwalletIds from ProposeRedeemCall |
| packages/sui-indexer/src/storage.ts | Added methods to retrieve confirming redeems, update redeem status, finalize redeems, and fetch redeem details with setup info; updated queries to include lc_pkg and lc_contract fields |
| packages/sui-indexer/src/rpc.ts | Implemented finalizeRedeems method that creates Sui clients and calls on-chain finalization for each redeem with proof |
| packages/sui-indexer/src/rpc-mocks.ts | Updated mock implementations to match new RPC interface |
| packages/sui-indexer/src/rpc-interface.ts | Changed finalizeRedeem to finalizeRedeems (array-based); added getConfirmingRedeems and updateRedeemStatus methods |
| packages/sui-indexer/src/redeem-sui-client.ts | Implemented finalizeRedeem method that submits Merkle proof to Sui light client contract |
| packages/btcindexer/src/index.ts | Simplified cron job by removing latestHeight parameter from updateConfirmationsAndFinalize call |
| packages/btcindexer/src/btcindexer.ts | Split updateConfirmationsAndFinalize into processMintingFinalization and processRedeemFinalization; added generic categorizeConfirmingTxs helper; renamed selectFinalizedNbtcTxs to splitActiveInactiveTxs |
| packages/btcindexer/src/btcindexer.test.ts | Updated tests for renamed methods and removed obsolete test cases |
| packages/btcindexer/src/btcindexer.helpers.test.ts | Updated test helpers to support new finalization flow |
robert-zaremba
left a comment
There was a problem hiding this comment.
pre-approve, but please do self review of this PR and check carefully your changes here.
Co-authored-by: Robert Zaremba <robert@zaremba.ch> Signed-off-by: sczembor <43810037+sczembor@users.noreply.github.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Description
Closes: #253
Author Checklist
All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.
I have...
!to the type prefix if API or client breaking changeCHANGELOG.mdSummary by Sourcery
Implement unified confirmation handling for BTC mint and redeem flows and wire Sui redeem finalization based on verified BTC confirmations and reorg detection.
New Features:
Bug Fixes:
Enhancements:
Tests: